---
title: "15：キャッシュ"
---

## 問題点

前のチュートリアルではレート制限機能を実装してアップストリームサービスを保護しましたが、これはトラフィックが設定したしきい値に達した時に以降のリクエストが保留されるため、ユーザーフレンドリーな体験ではありませんでした。バックエンドの開発においてはしばしばキャッシュを使用してデータベースへの読み書き速度を上げますが、これをプロキシにも適用します。静的なコンテンツや変更頻度が低いレスポンスの場合には、レスポンスをプロキシ層でキャッシュして、ユーザー体験を損なうことなくアップストリームとネットワークの負荷を軽減できます。

このチュートリアルではプロキシサーバーに *caching* 機能を追加します。

## 設定

このチュートリアルでは、 *service-hi* ターゲットサービスに静的なコンテンツの無期限のキャッシング機能を追加します。

``` js
//cache.json
{
  "services": {
    "service-hi": [
      ".js",
      ".json"
    ]
  },
  "timeout": 10000
}
```

ここで `timeout` はグローバル変数として設定していて何にでも適用可能で、必要であれば特定のサービス用に設定できます。

> *service-hi* を選んだのには特別な理由がいくつかあり、それを簡単に説明します。

## コードの説明

まずいくつかのグローバル変数を定義する必要があります。

1.	*_cache* はマップのタイプで、レスポンスの保存に使用します。
2.	*_cachedKey* はレスポンスをキャッシュに格納するためのキーです。今回は *host* と *path* の組み合わせをキーとして使用し、すべてのアップストリームノードのレスポンスをキャッシュします。
3.	*_cachedResponse* はキャッシュから取得した、キャッシュしたレスポンスです。
4.	*_useCache* はキャッシュを使用すべきか否かを示すフラグです。

またモジュールからも変数をインポートする必要があります。

``` js
pipy({
  _cache: {},
  _cachedKey: '',
  _cachedResponse: null,
  _useCache: false,
})

.import({
  __turnDown: 'proxy',
  __serviceID: 'router',
})
```

それではリクエストの処理を始めましょうか? まず *response* サブパイプラインにキャッシングを追加しましょう。

レスポンスのステータスコードの決定に加えて（成功したレスポンス、すなわちステータスコード2xxや3xxを返すレスポンスのみをキャッシュします）、キャッシュ時間も記録します。

``` js
.pipeline('response')
  .handleMessage(
    msg => (
      _useCache && (msg.head.status || 200) < 400 && (
        _cache[_cachedKey] = {
          time: Date.now(),
          message: msg,
        }
      )
    )
  )
```

リクエストを受信した後、キャッシュしたキーをリクエストヘッダーから取得し、次にリクエストをチェックしてキャッシュを使用しているかどうかを見ます。キャッシングが有効化されていれば、キーを使ってレスポンスをキャッシュからフェッチします。キャッシュを終了させるかどうかを決定する必要もある点に注意してください。
最後にレスポンスがローカルキャッシュで使用できるかどうかをチェックし、（キャッシュ無しで）そのリクエストで継続するか、またはターゲットサービスをヒットすることなくキャッシュしたレスポンスを直接クライアントに返すかを決定します。

``` js
.pipeline('request')
  .handleMessageStart(
    msg => (
      ((
        host, path
      ) => (
        host = msg.head.headers.host,
        path = msg.head.path,
        _useCache = config.services[__serviceID]?.some?.(
          ext => path.endsWith(ext)
        ),
        _useCache && (
          _cachedKey = host + path,
          _cachedResponse = _cache[_cachedKey],
          _cachedResponse?.time < Date.now() - config.timeout && (
            _cachedResponse = _cache[_cacheKey] = null
          ),
          _cachedResponse && (__turnDown = true)
        )
      ))()
    )
  )
  .link(
    'cache', () => Boolean(_cachedResponse),
    'bypass'
  )

.pipeline('cache')
  .replaceMessage(
    () => _cachedResponse.message
  )

.pipeline('bypass')
```

このモジュールを有効化してテストの問題を回避するには、レート制限機能を取り外した方が良いでしょう。

## テストしてみる

*service-hi* は2つのターゲットのエンドポイントで設定されていて、それぞれが別々のレスポンスを返します。`localhost:8000/hi/a.json` をリクエストすると、最初の10秒で最初のターゲットから返されたレスポンスを受信します。10秒の有効期限が経過した後は、レスポンスのコンテンツは第2のエンドポイントのものに変更されます。これはキャッシュの期限が切れてPipyがリクエストを第2のエンドポイントにフォーワードし、そのレスポンスをキャッシュしたためです。

``` shell
curl localhost:8000/hi/a.json
Hi, there!
curl localhost:8000/hi/a.json
Hi, there!
#wait > 10s
curl localhost:8000/hi/a.json
You are requesting /hi/a.json from ::ffff:127.0.0.1
```

## まとめ

このチュートリアルでは新しいフィルターを導入せず、最小限のコードでキャッシング機能を実装しました。以前説明したように、キャッシング機能はさまざまなサービスに合わせてカスタマイズでき、有効期限が異なる個別のサービスやパスレベルでのキャッシュの設定をより詳細に行うことができます。

### 次のパートの内容

このチュートリアルではパス `/hi/a.json` のキャッシングを実装しましたが、そのようなファイルは存在しません。次のチュートリアルではPipyのビルトイン機能を使ってHTTP静的ファイルサーバーを実装します。
